/*
     Copyright 2007 Joseph M. Ferner

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using log4net;
using OpenServers.Net.Common.FileSystem;
using OpenServers.Net.Common.Security;
using OpenServers.Net.Common.System;
using OpenServers.Net.Common.User;
using OpenServers.Net.FtpServer.State;

namespace OpenServers.Net.FtpServer.Command {
    /// <summary>
    ///     <para>
    ///         <b>LIST (LIST)</b><br/>
    ///         This command causes a list to be sent from the server to the
    ///         passive DTP.  If the pathname specifies a directory or other
    ///         group of files, the server should transfer a list of files
    ///         in the specified directory.  If the pathname specifies a
    ///         file then the server should send current information on the
    ///         file.  A null argument implies the user's current working or
    ///         default directory.  The data transfer is over the data
    ///         connection in type ASCII or type EBCDIC.  (The user must
    ///         ensure that the TYPE is appropriately ASCII or EBCDIC).
    ///         Since the information on a file may vary widely from system
    ///         to system, this information may be hard to use automatically
    ///         in a program, but may be quite useful to a human user.
    ///     </para>
    ///     <para>
    ///         <b>Format</b><br/>
    ///         <code>LIST [&lt;SP&gt; &lt;pathname&gt;] &lt;CRLF&gt;</code>
    ///     </para>
    ///     <para>
    ///         <b>Success:</b>
    ///         <list type="bullet">
    ///             <item><term>125</term><description>Data connection already open; transfer starting.</description></item>
    ///             <item><term>150</term><description>File status okay; about to open data connection.</description></item>
    ///             <item><term>226</term><description>Closing data connection. Requested file action successful (for example, filetransfer or file abort).</description></item>
    ///             <item><term>250</term><description>Requested file action okay, completed.</description></item>
    ///         </list>
    ///     </para>
    ///     <para>
    ///         <b>Errors:</b>
    ///         <list type="bullet">
    ///             <item><term>421</term><description>Service not available, closing control connection.</description></item>
    ///             <item><term>425</term><description>Can't open data connection.</description></item>
    ///             <item><term>426</term><description>Connection closed; transfer aborted.</description></item>
    ///             <item><term>450</term><description>Requested file action not taken. File unavailable (e.g., file busy).</description></item>
    ///             <item><term>451</term><description>Requested action aborted. Local error in processing.</description></item>
    ///             <item><term>500</term><description>Syntax error, command unrecognized.</description></item>
    ///             <item><term>501</term><description>Syntax error in parameters or arguments.</description></item>
    ///             <item><term>502</term><description>Command not implemented.</description></item>
    ///             <item><term>530</term><description>Not logged in.</description></item>
    ///         </list>
    ///     </para>
    /// </summary>
    public class ListCommand : BaseSendDataCommand {
        private static readonly ILog log = LogManager.GetLogger(typeof(ListCommand));

        private static readonly ISystem system = new DefaultSystem();

        /// <summary>
        /// returns a list of files.
        /// </summary>
        public override void PerformCommand(IFtpServer server, FtpState state, string arguments) {
            string filePath;
            if (arguments != null && arguments.Length > 0) {
                filePath = GetFilePath(state, arguments);
            } else {
                filePath = state.CurrentDirectory;
            }
            IDirectory directory = server.FileSystem.GetDirectory(filePath);
            if (directory == null || !directory.Exists) {
                state.Listener.Send(state, server.Messages.Get550(Reason550.FileNotFound));
            } else {
                if (server.SecurityProvider.CanRead(state.User, directory)) {
                    IList<IFileSystemObject> children = directory.GetItems();
                    StringBuilder fileListString = new StringBuilder();
                    FormatAsUnix(server.SecurityProvider, state.User, children, fileListString);
                    state.Listener.Send(state, server.Messages.Get150(filePath));
                    log.DebugFormat("begin sending [{0}]", filePath);
                    Stream fileListStream = new MemoryStream(ASCIIEncoding.ASCII.GetBytes(fileListString.ToString()));
                    SendData(server, state, fileListStream);
                } else {
                    state.Listener.Send(state, server.Messages.Get550(Reason550.AccessDenied));
                }
            }
        }

        /// <summary>
        /// -rw-r--r--    1 vglass   vglass       3651 Feb  2  2002 .screenrc
        /// drwxr-xr-x    3 vglass   vglass       4096 Jul 23 17:42 Archive
        /// -rw-r--r--    1 vglass   vglass       1096 Jul 23 13:35 GZipTest1.class
        /// -rw-r--r--    1 vglass   vglass        733 Jul 23 13:35 GZipTest1.java
        /// </summary>
        private void FormatAsUnix(ISecurityProvider securityProvider, IUser user, IEnumerable<IFileSystemObject> children, StringBuilder fileListString) {
            foreach (IFileSystemObject child in children) {
                string permissions = FormatAsUnixPermissions(securityProvider, user, child);
                string owner = "none";
                string group = "none";
                long size = 0;
                if (child is IFile) {
                    size = ((IFile)child).Size;
                }
                string date = FormatAsUnixDate(child.LastModified);
                string fileName = child.Name;
                int subDirectoryCount = 1;
                if (child is IDirectory) {
                    subDirectoryCount = ((IDirectory)child).GetDirectories().Count;
                }
                fileListString.Append(string.Format("{0} {1,4} {2,8} {3,8} {4,10} {5} {6}\r\n", permissions, subDirectoryCount, owner, group, size, date, fileName));
            }
        }

        /// <summary>
        /// If the year is different then the current year should return "Feb  2  2002".
        /// If the year is the same should return "Jul 23 17:42".
        /// </summary>
        private string FormatAsUnixDate(DateTime modified) {
            string dayWithOutZero = string.Format("{0:dd}", modified);
            if (dayWithOutZero.StartsWith("0")) {
                dayWithOutZero = dayWithOutZero.Substring(1);
            }
            if (system.DateTimeNow.Year != modified.Year) {
                return string.Format("{0:MMM} {1,2} {0,5:yyyy}", modified, dayWithOutZero);
            } else {
                return string.Format("{0:MMM} {1,2} {0:hh}:{0:mm}", modified, dayWithOutZero);
            }
        }

        /// <summary>
        /// drwxr-xr-x
        /// </summary>
        private string FormatAsUnixPermissions(ISecurityProvider securityProvider, IUser user, IFileSystemObject child) {
            bool directoryFlag = child is IDirectory;
            bool read = securityProvider.CanRead(user, child);
            bool write = securityProvider.CanWrite(user, child);
            string userPermissions = string.Format("{0}{1}-", read ? "r" : "-", write ? "w" : "-");
            string groupPermissions = string.Format("{0}{1}-", read ? "r" : "-", write ? "w" : "-");
            string allPermissions = string.Format("{0}{1}-", read ? "r" : "-", write ? "w" : "-");
            return string.Format("{0}{1}{2}{3}", directoryFlag ? "d" : "-", userPermissions, groupPermissions, allPermissions);
        }
    }
}
